<?php
/**
 * Smarty Resource Plugin
 *
 * @package    Smarty
 * @subpackage TemplateResources
 * @author     Rodney Rehm
 */

/**
 * Smarty Resource Plugin
 *
 * Base implementation for resource plugins
 *
 * @package    Smarty
 * @subpackage TemplateResources
 */
abstract class Smarty_Resource {
	/**
	 * cache for Smarty_Template_Source instances
	 *
	 * @var array
	 */
	public static $sources = array();
	/**
	 * cache for Smarty_Template_Compiled instances
	 *
	 * @var array
	 */
	public static $compileds = array();
	/**
	 * cache for Smarty_Resource instances
	 *
	 * @var array
	 */
	public static $resources = array();
	/**
	 * resource types provided by the core
	 *
	 * @var array
	 */
	protected static $sysplugins = array(
			'file' => true,
			'string' => true,
			'extends' => true,
			'stream' => true,
			'eval' => true,
			'php' => true
	);

	/**
	 * Name of the Class to compile this resource's contents with
	 *
	 * @var string
	 */
	public $compiler_class = 'Smarty_Internal_SmartyTemplateCompiler';

	/**
	 * Name of the Class to tokenize this resource's contents with
	 *
	 * @var string
	 */
	public $template_lexer_class = 'Smarty_Internal_Templatelexer';

	/**
	 * Name of the Class to parse this resource's contents with
	 *
	 * @var string
	 */
	public $template_parser_class = 'Smarty_Internal_Templateparser';

	/**
	 * Load template's source into current template object
	 *
	 * {@internal The loaded source is assigned to $_template->source->content directly.}}
	 *
	 * @param Smarty_Template_Source $source source object
	 *
	 * @return string template source
	 * @throws SmartyException if source cannot be loaded
	 */
	public abstract function getContent(Smarty_Template_Source $source);

	/**
	 * populate Source Object with meta data from Resource
	 *
	 * @param Smarty_Template_Source   $source    source object
	 * @param Smarty_Internal_Template $_template template object
	 */
	public abstract function populate(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null);

	/**
	 * populate Source Object with timestamp and exists from Resource
	 *
	 * @param Smarty_Template_Source $source source object
	 */
	public function populateTimestamp(Smarty_Template_Source $source) {
		// intentionally left blank
	}


	/**
	 * modify resource_name according to resource handlers specifications
	 *
	 * @param Smarty $smarty        Smarty instance
	 * @param string $resource_name resource_name to make unique
	 *
	 * @return string unique resource name
	 */
	protected function buildUniqueResourceName(Smarty $smarty, $resource_name) {
		return get_class($this) . '#' . $smarty->joined_template_dir . '#' . $resource_name;
	}

	/**
	 * populate Compiled Object with compiled filepath
	 *
	 * @param Smarty_Template_Compiled $compiled  compiled object
	 * @param Smarty_Internal_Template $_template template object
	 */
	public function populateCompiledFilepath(Smarty_Template_Compiled $compiled, Smarty_Internal_Template $_template) {
		$_compile_id = isset($_template->compile_id) ? preg_replace('![^\w\|]+!', '_', $_template->compile_id) : null;
		$_filepath = $compiled->source->uid;
		// if use_sub_dirs, break file into directories
		if ($_template->smarty->use_sub_dirs)
		{
			$_filepath = substr($_filepath, 0, 2) . DS . substr($_filepath, 2, 2) . DS . substr($_filepath, 4, 2) . DS . $_filepath;
		}
		$_compile_dir_sep = $_template->smarty->use_sub_dirs ? DS : '^';
		if (isset($_compile_id))
		{
			$_filepath = $_compile_id . $_compile_dir_sep . $_filepath;
		}
		// caching token
		if ($_template->caching)
		{
			$_cache = '.cache';
		}
		else
		{
			$_cache = '';
		}
		$_compile_dir = $_template->smarty->getCompileDir();
		// set basename if not specified
		$_basename = $this->getBasename($compiled->source);
		if ($_basename === null)
		{
			$_basename = basename(preg_replace('![^\w\/]+!', '_', $compiled->source->name));
		}
		// separate (optional) basename by dot
		if ($_basename)
		{
			$_basename = '.' . $_basename;
		}

		$compiled->filepath = $_compile_dir . $_filepath . '.' . $compiled->source->type . $_basename . $_cache . '.php';
	}

	/**
	 * Normalize Paths "foo/../bar" to "bar"
	 *
	 * @param string  $_path path to normalize
	 * @param boolean $ds    respect windows directory separator
	 *
	 * @return string normalized path
	 */
	protected function normalizePath($_path, $ds = true) {
		if ($ds)
		{
			// don't we all just love windows?
			$_path = str_replace('\\', '/', $_path);
		}

		$offset = 0;

		// resolve simples
		$_path = preg_replace('#(/\./(\./)*)|/{2,}#', '/', $_path);
		// resolve parents
		while (true)
		{
			$_parent = strpos($_path, '/../', $offset);
			if (!$_parent)
			{
				break;
			}
			else if ($_path[$_parent - 1] === '.')
			{
				$offset = $_parent + 3;
				continue;
			}

			$_pos = strrpos($_path, '/', $_parent - strlen($_path) - 1);
			if ($_pos === false)
			{
				// don't we all just love windows?
				$_pos = $_parent;
			}

			$_path = substr_replace($_path, '', $_pos, $_parent + 3 - $_pos);
		}

		if ($ds && DS != '/')
		{
			// don't we all just love windows?
			$_path = str_replace('/', '\\', $_path);
		}

		return $_path;
	}

	/**
	 * build template filepath by traversing the template_dir array
	 *
	 * @param Smarty_Template_Source   $source    source object
	 * @param Smarty_Internal_Template $_template template object
	 *
	 * @return string fully qualified filepath
	 * @throws SmartyException if default template handler is registered but not callable
	 */
	protected function buildFilepath(Smarty_Template_Source $source, Smarty_Internal_Template $_template = null) {
		$file = $source->name;
		if ($source instanceof Smarty_Config_Source)
		{
			$_directories = $source->smarty->getConfigDir();
			$_default_handler = $source->smarty->default_config_handler_func;
		}
		else
		{
			$_directories = $source->smarty->getTemplateDir();
			$_default_handler = $source->smarty->default_template_handler_func;
		}

		// go relative to a given template?
		$_file_is_dotted = $file[0] == '.' && ($file[1] == '.' || $file[1] == '/' || $file[1] == "\\");
		if ($_template && $_template->parent instanceof Smarty_Internal_Template && $_file_is_dotted)
		{
			if ($_template->parent->source->type != 'file' && $_template->parent->source->type != 'extends' && !$_template->parent->allow_relative_path)
			{
				throw new SmartyException("Template '{$file}' cannot be relative to template of resource type '{$_template->parent->source->type}'");
			}
			$file = dirname($_template->parent->source->filepath) . DS . $file;
			$_file_exact_match = true;
			if (!preg_match('/^([\/\\\\]|[a-zA-Z]:[\/\\\\])/', $file))
			{
				// the path gained from the parent template is relative to the current working directory
				// as expansions (like include_path) have already been done
				$file = getcwd() . DS . $file;
			}
		}

		// resolve relative path
		if (!preg_match('/^([\/\\\\]|[a-zA-Z]:[\/\\\\])/', $file))
		{
			// don't we all just love windows?
			$_path = str_replace('\\', '/', $file);
			$_path = DS . trim($file, '/');
			$_was_relative = true;
		}
		else
		{
			// don't we all just love windows?
			$_path = str_replace('\\', '/', $file);
		}
		$_path = $this->normalizePath($_path, false);
		if (DS != '/')
		{
			// don't we all just love windows?
			$_path = str_replace('/', '\\', $_path);
		}
		// revert to relative
		if (isset($_was_relative))
		{
			$_path = substr($_path, 1);
		}

		// this is only required for directories
		$file = rtrim($_path, '/\\');

		// files relative to a template only get one shot
		if (isset($_file_exact_match))
		{
			return $this->fileExists($source, $file) ? $file : false;
		}

		// template_dir index?
		if (preg_match('#^\[(?P<key>[^\]]+)\](?P<file>.+)$#', $file, $match))
		{
			$_directory = null;
			// try string indexes
			if (isset($_directories[$match['key']]))
			{
				$_directory = $_directories[$match['key']];
			}
			else if (is_numeric($match['key']))
			{
				// try numeric index
				$match['key'] = (int)$match['key'];
				if (isset($_directories[$match['key']]))
				{
					$_directory = $_directories[$match['key']];
				}
				else
				{
					// try at location index
					$keys = array_keys($_directories);
					$_directory = $_directories[$keys[$match['key']]];
				}
			}

			if ($_directory)
			{
				$_file = substr($file, strpos($file, ']') + 1);
				$_filepath = $_directory . $_file;
				if ($this->fileExists($source, $_filepath))
				{
					return $_filepath;
				}
			}
		}

		$_stream_resolve_include_path = function_exists('stream_resolve_include_path');

		// relative file name?
		if (!preg_match('/^([\/\\\\]|[a-zA-Z]:[\/\\\\])/', $file))
		{
			foreach ($_directories as $_directory)
			{
				$_filepath = $_directory . $file;
				if ($this->fileExists($source, $_filepath))
				{
					return $this->normalizePath($_filepath);
				}
				if ($source->smarty->use_include_path && !preg_match('/^([\/\\\\]|[a-zA-Z]:[\/\\\\])/', $_directory))
				{
					// try PHP include_path
					if ($_stream_resolve_include_path)
					{
						$_filepath = stream_resolve_include_path($_filepath);
					}
					else
					{
						$_filepath = Smarty_Internal_Get_Include_Path::getIncludePath($_filepath);
					}

					if ($_filepath !== false)
					{
						if ($this->fileExists($source, $_filepath))
						{
							return $this->normalizePath($_filepath);
						}
					}
				}
			}
		}

		// try absolute filepath
		if ($this->fileExists($source, $file))
		{
			return $file;
		}

		// no tpl file found
		if ($_default_handler)
		{
			if (!is_callable($_default_handler))
			{
				if ($source instanceof Smarty_Config_Source)
				{
					throw new SmartyException("Default config handler not callable");
				}
				else
				{
					throw new SmartyException("Default template handler not callable");
				}
			}
			$_return = call_user_func_array($_default_handler, array($source->type, $source->name, &$_content, &$_timestamp, $source->smarty));
			if (is_string($_return))
			{
				$source->timestamp = @filemtime($_return);
				$source->exists = !!$source->timestamp;

				return $_return;
			}
			elseif ($_return === true)
			{
				$source->content = $_content;
				$source->timestamp = $_timestamp;
				$source->exists = true;

				return $_filepath;
			}
		}

		// give up
		return false;
	}

	/**
	 * test is file exists and save timestamp
	 *
	 * @param Smarty_Template_Source $source source object
	 * @param string                 $file   file name
	 *
	 * @return bool  true if file exists
	 */
	protected function fileExists(Smarty_Template_Source $source, $file) {
		$source->timestamp = @filemtime($file);

		return $source->exists = !!$source->timestamp;

	}

	/**
	 * Determine basename for compiled filename
	 *
	 * @param Smarty_Template_Source $source source object
	 *
	 * @return string resource's basename
	 */
	protected function getBasename(Smarty_Template_Source $source) {
		return null;
	}

	/**
	 * Load Resource Handler
	 *
	 * @param Smarty $smarty smarty object
	 * @param string $type   name of the resource
	 *
	 * @return Smarty_Resource Resource Handler
	 */
	public static function load(Smarty $smarty, $type) {
		// try smarty's cache
		if (isset($smarty->_resource_handlers[$type]))
		{
			return $smarty->_resource_handlers[$type];
		}

		// try registered resource
		if (isset($smarty->registered_resources[$type]))
		{
			if ($smarty->registered_resources[$type] instanceof Smarty_Resource)
			{
				$smarty->_resource_handlers[$type] = $smarty->registered_resources[$type];

				// note registered to smarty is not kept unique!
				return $smarty->_resource_handlers[$type];
			}

			if (!isset(self::$resources['registered']))
			{
				self::$resources['registered'] = new Smarty_Internal_Resource_Registered();
			}
			if (!isset($smarty->_resource_handlers[$type]))
			{
				$smarty->_resource_handlers[$type] = self::$resources['registered'];
			}

			return $smarty->_resource_handlers[$type];
		}

		// try sysplugins dir
		if (isset(self::$sysplugins[$type]))
		{
			if (!isset(self::$resources[$type]))
			{
				$_resource_class = 'Smarty_Internal_Resource_' . ucfirst($type);
				self::$resources[$type] = new $_resource_class();
			}

			return $smarty->_resource_handlers[$type] = self::$resources[$type];
		}

		// try plugins dir
		$_resource_class = 'Smarty_Resource_' . ucfirst($type);
		if ($smarty->loadPlugin($_resource_class))
		{
			if (isset(self::$resources[$type]))
			{
				return $smarty->_resource_handlers[$type] = self::$resources[$type];
			}

			if (class_exists($_resource_class, false))
			{
				self::$resources[$type] = new $_resource_class();

				return $smarty->_resource_handlers[$type] = self::$resources[$type];
			}
			else
			{
				$smarty->registerResource($type, array(
						"smarty_resource_{$type}_source",
						"smarty_resource_{$type}_timestamp",
						"smarty_resource_{$type}_secure",
						"smarty_resource_{$type}_trusted"
				));

				// give it another try, now that the resource is registered properly
				return self::load($smarty, $type);
			}
		}

		// try streams
		$_known_stream = stream_get_wrappers();
		if (in_array($type, $_known_stream))
		{
			// is known stream
			if (is_object($smarty->security_policy))
			{
				$smarty->security_policy->isTrustedStream($type);
			}
			if (!isset(self::$resources['stream']))
			{
				self::$resources['stream'] = new Smarty_Internal_Resource_Stream();
			}

			return $smarty->_resource_handlers[$type] = self::$resources['stream'];
		}

		// TODO: try default_(template|config)_handler

		// give up
		throw new SmartyException("Unkown resource type '{$type}'");
	}

	/**
	 * extract resource_type and resource_name from template_resource and config_resource
	 *
	 * @note "C:/foo.tpl" was forced to file resource up till Smarty 3.1.3 (including).
	 * @param string $resource_name    template_resource or config_resource to parse
	 * @param string $default_resource the default resource_type defined in $smarty
	 * @param string &$name            the parsed resource name
	 * @param string &$type            the parsed resource type
	 *
	 * @return void
	 */
	protected static function parseResourceName($resource_name, $default_resource, &$name, &$type) {
		$parts = explode(':', $resource_name, 2);
		if (!isset($parts[1]) || !isset($parts[0][1]))
		{
			// no resource given, use default
			// or single character before the colon is not a resource type, but part of the filepath
			$type = $default_resource;
			$name = $resource_name;
		}
		else
		{
			$type = $parts[0];
			$name = $parts[1];
		}
	}


	/**
	 * modify resource_name according to resource handlers specifications
	 *
	 * @param Smarty $smarty        Smarty instance
	 * @param string $resource_name resource_name to make unique
	 *
	 * @return string unique resource name
	 */

	/**
	 * modify template_resource according to resource handlers specifications
	 *
	 * @param string $smarty            Smarty instance
	 * @param string $template_resource template_resource to extracate resource handler and name of
	 *
	 * @return string unique resource name
	 */
	public static function getUniqueTemplateName($smarty, $template_resource) {
		self::parseResourceName($template_resource, $smarty->default_resource_type, $name, $type);
		// TODO: optimize for Smarty's internal resource types
		$resource = Smarty_Resource::load($smarty, $type);

		return $resource->buildUniqueResourceName($smarty, $name);
	}

	/**
	 * initialize Source Object for given resource
	 *
	 * Either [$_template] or [$smarty, $template_resource] must be specified
	 *
	 * @param Smarty_Internal_Template $_template         template object
	 * @param Smarty                   $smarty            smarty object
	 * @param string                   $template_resource resource identifier
	 *
	 * @return Smarty_Template_Source Source Object
	 */
	public static function source(Smarty_Internal_Template $_template = null, Smarty $smarty = null, $template_resource = null) {
		if ($_template)
		{
			$smarty = $_template->smarty;
			$template_resource = $_template->template_resource;
		}

		// parse resource_name, load resource handler, identify unique resource name
		self::parseResourceName($template_resource, $smarty->default_resource_type, $name, $type);
		$resource = Smarty_Resource::load($smarty, $type);
		$unique_resource_name = $resource->buildUniqueResourceName($smarty, $name);

		// check runtime cache
		$_cache_key = 'template|' . $unique_resource_name;
		if ($smarty->compile_id)
		{
			$_cache_key .= '|' . $smarty->compile_id;
		}
		if (isset(self::$sources[$_cache_key]))
		{
			return self::$sources[$_cache_key];
		}

		// create source
		$source = new Smarty_Template_Source($resource, $smarty, $template_resource, $type, $name, $unique_resource_name);
		$resource->populate($source, $_template);

		// runtime cache
		self::$sources[$_cache_key] = $source;

		return $source;
	}

	/**
	 * initialize Config Source Object for given resource
	 *
	 * @param Smarty_Internal_Config $_config config object
	 *
	 * @return Smarty_Config_Source Source Object
	 */
	public static function config(Smarty_Internal_Config $_config) {
		static $_incompatible_resources = array('eval' => true, 'string' => true, 'extends' => true, 'php' => true);
		$config_resource = $_config->config_resource;
		$smarty = $_config->smarty;

		// parse resource_name
		self::parseResourceName($config_resource, $smarty->default_config_type, $name, $type);

		// make sure configs are not loaded via anything smarty can't handle
		if (isset($_incompatible_resources[$type]))
		{
			throw new SmartyException ("Unable to use resource '{$type}' for config");
		}

		// load resource handler, identify unique resource name
		$resource = Smarty_Resource::load($smarty, $type);
		$unique_resource_name = $resource->buildUniqueResourceName($smarty, $name);

		// check runtime cache
		$_cache_key = 'config|' . $unique_resource_name;
		if (isset(self::$sources[$_cache_key]))
		{
			return self::$sources[$_cache_key];
		}

		// create source
		$source = new Smarty_Config_Source($resource, $smarty, $config_resource, $type, $name, $unique_resource_name);
		$resource->populate($source, null);

		// runtime cache
		self::$sources[$_cache_key] = $source;

		return $source;
	}

}

/**
 * Smarty Resource Data Object
 *
 * Meta Data Container for Template Files
 *
 * @package    Smarty
 * @subpackage TemplateResources
 * @author     Rodney Rehm
 *
 * @property integer $timestamp Source Timestamp
 * @property boolean $exists    Source Existance
 * @property boolean $template  Extended Template reference
 * @property string  $content   Source Content
 */
class Smarty_Template_Source {

	/**
	 * Name of the Class to compile this resource's contents with
	 *
	 * @var string
	 */
	public $compiler_class = null;

	/**
	 * Name of the Class to tokenize this resource's contents with
	 *
	 * @var string
	 */
	public $template_lexer_class = null;

	/**
	 * Name of the Class to parse this resource's contents with
	 *
	 * @var string
	 */
	public $template_parser_class = null;

	/**
	 * Unique Template ID
	 *
	 * @var string
	 */
	public $uid = null;

	/**
	 * Template Resource (Smarty_Internal_Template::$template_resource)
	 *
	 * @var string
	 */
	public $resource = null;

	/**
	 * Resource Type
	 *
	 * @var string
	 */
	public $type = null;

	/**
	 * Resource Name
	 *
	 * @var string
	 */
	public $name = null;

	/**
	 * Unique Resource Name
	 *
	 * @var string
	 */
	public $unique_resource = null;

	/**
	 * Source Filepath
	 *
	 * @var string
	 */
	public $filepath = null;

	/**
	 * Source is bypassing compiler
	 *
	 * @var boolean
	 */
	public $uncompiled = null;

	/**
	 * Source must be recompiled on every occasion
	 *
	 * @var boolean
	 */
	public $recompiled = null;

	/**
	 * The Components an extended template is made of
	 *
	 * @var array
	 */
	public $components = null;

	/**
	 * Resource Handler
	 *
	 * @var Smarty_Resource
	 */
	public $handler = null;

	/**
	 * Smarty instance
	 *
	 * @var Smarty
	 */
	public $smarty = null;

	/**
	 * create Source Object container
	 *
	 * @param Smarty_Resource $handler         Resource Handler this source object communicates with
	 * @param Smarty          $smarty          Smarty instance this source object belongs to
	 * @param string          $resource        full template_resource
	 * @param string          $type            type of resource
	 * @param string          $name            resource name
	 * @param string          $unique_resource unqiue resource name
	 */
	public function __construct(Smarty_Resource $handler, Smarty $smarty, $resource, $type, $name, $unique_resource) {
		$this->handler = $handler; // Note: prone to circular references

		$this->compiler_class = $handler->compiler_class;
		$this->template_lexer_class = $handler->template_lexer_class;
		$this->template_parser_class = $handler->template_parser_class;
		$this->uncompiled = $this->handler instanceof Smarty_Resource_Uncompiled;
		$this->recompiled = $this->handler instanceof Smarty_Resource_Recompiled;

		$this->smarty = $smarty;
		$this->resource = $resource;
		$this->type = $type;
		$this->name = $name;
		$this->unique_resource = $unique_resource;
	}

	/**
	 * get a Compiled Object of this source
	 *
	 * @param Smarty_Internal_Template $_template template objet
	 *
	 * @return Smarty_Template_Compiled compiled object
	 */
	public function getCompiled(Smarty_Internal_Template $_template) {
		// check runtime cache
		$_cache_key = $this->unique_resource . '#' . $_template->compile_id;
		if (isset(Smarty_Resource::$compileds[$_cache_key]))
		{
			return Smarty_Resource::$compileds[$_cache_key];
		}

		$compiled = new Smarty_Template_Compiled($this);
		$this->handler->populateCompiledFilepath($compiled, $_template);
		$compiled->timestamp = @filemtime($compiled->filepath);
		$compiled->exists = !!$compiled->timestamp;

		// runtime cache
		Smarty_Resource::$compileds[$_cache_key] = $compiled;

		return $compiled;
	}

	/**
	 * render the uncompiled source
	 *
	 * @param Smarty_Internal_Template $_template template object
	 */
	public function renderUncompiled(Smarty_Internal_Template $_template) {
		return $this->handler->renderUncompiled($this, $_template);
	}

	/**
	 * <<magic>> Generic Setter.
	 *
	 * @param string $property_name valid: timestamp, exists, content, template
	 * @param mixed  $value         new value (is not checked)
	 *
	 * @throws SmartyException if $property_name is not valid
	 */
	public function __set($property_name, $value) {
		switch ($property_name)
		{
			// regular attributes
			case 'timestamp':
			case 'exists':
			case 'content':
				// required for extends: only
			case 'template':
				$this->$property_name = $value;
				break;

			default:
				throw new SmartyException("invalid source property '$property_name'.");
		}
	}

	/**
	 * <<magic>> Generic getter.
	 *
	 * @param string $property_name valid: timestamp, exists, content
	 *
	 * @return mixed
	 * @throws SmartyException if $property_name is not valid
	 */
	public function __get($property_name) {
		switch ($property_name)
		{
			case 'timestamp':
			case 'exists':
				$this->handler->populateTimestamp($this);

				return $this->$property_name;

			case 'content':
				return $this->content = $this->handler->getContent($this);

			default:
				throw new SmartyException("source property '$property_name' does not exist.");
		}
	}

}

/**
 * Smarty Resource Data Object
 *
 * Meta Data Container for Template Files
 *
 * @package    Smarty
 * @subpackage TemplateResources
 * @author     Rodney Rehm
 *
 * @property string $content compiled content
 */
class Smarty_Template_Compiled {

	/**
	 * Compiled Filepath
	 *
	 * @var string
	 */
	public $filepath = null;

	/**
	 * Compiled Timestamp
	 *
	 * @var integer
	 */
	public $timestamp = null;

	/**
	 * Compiled Existance
	 *
	 * @var boolean
	 */
	public $exists = false;

	/**
	 * Compiled Content Loaded
	 *
	 * @var boolean
	 */
	public $loaded = false;

	/**
	 * Template was compiled
	 *
	 * @var boolean
	 */
	public $isCompiled = false;

	/**
	 * Source Object
	 *
	 * @var Smarty_Template_Source
	 */
	public $source = null;

	/**
	 * Metadata properties
	 *
	 * populated by Smarty_Internal_Template::decodeProperties()
	 *
	 * @var array
	 */
	public $_properties = null;

	/**
	 * create Compiled Object container
	 *
	 * @param Smarty_Template_Source $source source object this compiled object belongs to
	 */
	public function __construct(Smarty_Template_Source $source) {
		$this->source = $source;
	}

}

?>
